﻿using System;
using System.Collections;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using rossisdead.Mvc.Detergent;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace MMSINC.CoreTest.ClassExtensions.MvcExtensions
{
    [TestClass]
    public class CascadingDropDownExtensionsHelperTest
    {
        #region Fields

        private TestCascadingDropDownExtensionsHelper _target;
        private ModelMetadataProvider _oldMetadataProvider;
        private IControllerFactory _oldControllerFactory;
        private ViewModel _model;

        #endregion

        #region Init/Cleanup

        [TestInitialize]
        public void InitializeTest()
        {
            _oldControllerFactory = ControllerBuilder.Current.GetControllerFactory();
            _oldMetadataProvider = ModelMetadataProviders.Current;
            ModelMetadataProviders.Current = new CustomModelMetadataProvider();
            _target = new TestCascadingDropDownExtensionsHelper();
            _model = new ViewModel();
        }

        [TestCleanup]
        public void CleanupTest()
        {
            ControllerBuilder.Current.SetControllerFactory(_oldControllerFactory);
            ModelMetadataProviders.Current = _oldMetadataProvider;
        }

        private CascadingAttribute CreateCascadingAttribute()
        {
            return new CascadingAttribute
                       {
                           Action = "Action",
                           Async = false,
                           DependsOn = "Depends on",
                           ErrorText = "Error text",
                           HttpMethod = "GET",
                           LoadingText = "Loading",
                           PromptText = "Prompt"
                       };
        }

        private HtmlHelper CreateHtmlHelper()
        {
            var viewdata = new ViewDataDictionary(_model);
            viewdata["Container"] = _model;
            var vdc = new Mock<IViewDataContainer>();
            vdc.Setup(x => x.ViewData).Returns(viewdata);
            var htmlHelper = new HtmlHelper(new ViewContext(), vdc.Object);
            return htmlHelper;
        }

        #endregion

        #region Tests

        #region ApplyOptionalHtmlAttribute

        private void TestApplyOptionalHtmlAttributeByValueNotSet(string value)
        {
            var attrs = new Dictionary<string, object>();
            _target.ApplyOptionalHtmlAttribute(value, "some-attribute", attrs);
            Assert.IsFalse(attrs.ContainsKey("some-attribute"));
        }

        [TestMethod]
        public void TestApplyOptionalHtmlAttributeAddsHtmlEncodedValueIfValueIsNotNullOrEmptyOrWhitespace()
        {
            TestApplyOptionalHtmlAttributeByValueNotSet(null);
            TestApplyOptionalHtmlAttributeByValueNotSet(string.Empty);
            TestApplyOptionalHtmlAttributeByValueNotSet("   ");
        }

        [TestMethod]
        public void TestApplyOptionalHtmlAttributesHtmlEncodesTheValue()
        {
            var attrs = new RouteValueDictionary();
            var expected = HttpUtility.HtmlAttributeEncode("oh? that's \" cool & stuff");
            _target.ApplyOptionalHtmlAttribute("oh? that's \" cool & stuff", "some-attribute", attrs);
            Assert.AreEqual(expected, attrs["some-attribute"]);
        }

        #endregion

        #region ApplyCascadingHtmlAttributes

        private void AssertApplyCascadingHtmlAttributeSet(string htmlAttributeName, Func<CascadingAttribute, string> propGetter, CascadingAttribute attr)
        {
            // RouteValueDictionary is what's used internally by the helper, so 
            // let's use that here, too.
            var dict = new RouteValueDictionary();
            var expected = HttpUtility.HtmlEncode(propGetter(attr));
            _target.ApplyCascadingHtmlAttributes(attr, dict);
            Assert.AreEqual(expected, dict[htmlAttributeName]);
        }

        [TestMethod]
        public void TestApplyCascadingHtmlAttributesSetsDataCascadingTrue()
        {
            AssertApplyCascadingHtmlAttributeSet("data-cascading", x => "true", CreateCascadingAttribute());
        }

        [TestMethod]
        public void TestApplyCascadingHtmlAttributesSetsAction()
        {
            AssertApplyCascadingHtmlAttributeSet("data-cascading-action", x => x.Action, CreateCascadingAttribute());
        }

        [TestMethod]
        public void TestApplyCascadingHtmlAttributesSetsDependsOn()
        {
            // Need to also ensure that the # is added to the field ID name.
            AssertApplyCascadingHtmlAttributeSet("data-cascading-dependson", x => "#" + x.DependsOn, CreateCascadingAttribute());
        }

        [TestMethod]
        public void TestApplyCascadingHtmlAttributesSetsErrorTextIfErrorTextIsSetOnAttribute()
        {
            AssertApplyCascadingHtmlAttributeSet("data-cascading-errortext", x => x.ErrorText, CreateCascadingAttribute());
        }

        [TestMethod]
        public void TestApplyCascadingHtmlAttributesDoesNotSetErrorTextIfNotSetOnAttribute()
        {
            var attr = CreateCascadingAttribute();
            attr.ErrorText = null;
            AssertApplyCascadingHtmlAttributeSet("data-cascading-errortext", x => x.ErrorText, attr);
        }

        [TestMethod]
        public void TestApplyCascadingHtmlAttributesSetsLoadingTextIfLoadingTextIsSetOnAttribute()
        {
            AssertApplyCascadingHtmlAttributeSet("data-cascading-loadingtext", x => x.LoadingText, CreateCascadingAttribute());
        }

        [TestMethod]
        public void TestApplyCascadingHtmlAttributesDoesNotSetLoadingTextIfNotSetOnAttribute()
        {
            var attr = CreateCascadingAttribute();
            attr.LoadingText = null;
            AssertApplyCascadingHtmlAttributeSet("data-cascading-loadingtext", x => x.LoadingText, attr);
        }

        [TestMethod]
        public void TestApplyCascadingHtmlAttributesSetsPromptTextIfPromptTextIsSetOnAttribute()
        {
            AssertApplyCascadingHtmlAttributeSet("data-cascading-prompttext", x => x.PromptText, CreateCascadingAttribute());
        }

        [TestMethod]
        public void TestApplyCascadingHtmlAttributesDoesNotSetPromptTextIfNotSetOnAttribute()
        {
            var attr = CreateCascadingAttribute();
            attr.PromptText = null;
            AssertApplyCascadingHtmlAttributeSet("data-cascading-prompttext", x => x.PromptText, attr);
        }

        [TestMethod]
        public void TestApplyCascadingHtmlAttributesSetsHttpMethodIfHttpMethodIsSetOnAttribute()
        {
            AssertApplyCascadingHtmlAttributeSet("data-cascading-httpmethod", x => x.HttpMethod, CreateCascadingAttribute());
        }

        [TestMethod]
        public void TestApplyCascadingHtmlAttributesDoesNotSetHttpMethodIfNotSetOnAttribute()
        {
            var attr = CreateCascadingAttribute();
            attr.HttpMethod = null;
            AssertApplyCascadingHtmlAttributeSet("data-cascading-httpmethod", x => x.HttpMethod, attr);
        }

        #endregion

        #region GetCascadingAttribute

        [TestMethod]
        public void TestGetCascadingAttributeReturnsCascadingAttributeForGivenProperty()
        {
            var attr = (CascadingAttribute)typeof(ViewModel).GetProperty("CascadingProperty").GetCustomAttributes(typeof(CascadingAttribute), false).Single();
            var result = _target.GetCascadingAttribute(CreateHtmlHelper(), "CascadingProperty");
            // For whatever reason, the attribute returned from reflection is not the same returned
            // by MVC's internal type descriptor. It instead returns a copy. The copy is the one
            // that gets passed to the Process method of ModelMetadataProvider.
            Assert.AreEqual(attr.Action, result.Action);
            Assert.AreEqual(attr.DependsOn, result.DependsOn);
            Assert.AreEqual("CascadingProperty", result.OwningProperty);
        }


        #endregion

        #region GetModelMetadata

        [TestMethod]
        public void TestGetModelMetadataReturnsExpectedMetadataIfModelIsOfContainerType()
        {
            var helper = CreateHtmlHelper();
            var metadata = _target.GetModelMetadata(helper, "CascadingProperty");
            Assert.AreSame(typeof(ViewModel), metadata.ContainerType);
            Assert.AreEqual("CascadingProperty", metadata.PropertyName);
        }

        #endregion

        #region GetSelectListItems

        [TestMethod]
        public void TestGetSelectListItemsWorksWhenTheParentModelIsNull()
        {
            Assert.Inconclusive("TODO");
        }

        [TestMethod]
        public void TestGetSelectListItemsThrowsExceptionIfViewDataDoesNotContainKeyContainerOnNonComplexModelTypes()
        {
            Assert.Inconclusive("TODO");
            var helper = CreateHtmlHelper();
            helper.ViewData.Remove("Container");
            DetergentTest.TestHelper.Throws<InvalidOperationException>(() => _target.GetSelectListItems(helper, null));
        }

        [TestMethod]
        public void TestGetSelectListItemsReturnsEmptyIfParentValueDoesNotHaveValue()
        {
            var helper = CreateHtmlHelper();
            var attr = _target.GetCascadingAttribute(helper, "CascadingProperty");
            Assert.AreSame(Enumerable.Empty<SelectListItem>(), _target.GetSelectListItems(helper, attr));
        }

        [TestMethod]
        public void TestGetSelectListItemReturnsItemFromControllerActionThatMatchesTheActionProperty()
        {
            var expectedController = new TestController();
            var expected = new List<SomeModel>();
            expected.Add(new SomeModel { Text = "One", Value = 1 });
            expected.Add(new SomeModel { Text = "Two", Value = 2 });
            expectedController.TestData = expected;

            var controllerFactory = new Mock<IControllerFactory>();
            controllerFactory.Setup(x => x.CreateController(It.IsAny<RequestContext>(), "TestController")).Returns(expectedController).Verifiable();
            ControllerBuilder.Current.SetControllerFactory(controllerFactory.Object);

            var helper = CreateHtmlHelper();
            var attr = _target.GetCascadingAttribute(helper, "CascadingProperty");
            _model.ParentProperty = 1;
            var items = _target.GetSelectListItems(helper, attr).ToArray();

            Assert.AreEqual(3, items.Count(), "There should be three items. The two in the expected list and one additional empty select value.");
            Assert.AreEqual("", items[0].Value);
            Assert.AreEqual("One", items[1].Text);
            Assert.AreEqual("1", items[1].Value);
            Assert.AreEqual("Two", items[2].Text);
            Assert.AreEqual("2", items[2].Value);

            controllerFactory.Verify();
        }

        [TestMethod]
        public void TestGetSelectListItemsDisposesController()
        {
            var expectedController = new TestController();
            var expected = new List<SomeModel>();
            expectedController.TestData = expected;

            var controllerFactory = new Mock<IControllerFactory>();
            controllerFactory.Setup(x => x.CreateController(It.IsAny<RequestContext>(), "TestController")).Returns(expectedController).Verifiable();
            ControllerBuilder.Current.SetControllerFactory(controllerFactory.Object);

            var helper = CreateHtmlHelper();
            var attr = _target.GetCascadingAttribute(helper, "CascadingProperty");
            _model.ParentProperty = 1;
// ReSharper disable ReturnValueOfPureMethodIsNotUsed
            _target.GetSelectListItems(helper, attr).ToArray();
// ReSharper restore ReturnValueOfPureMethodIsNotUsed
            controllerFactory.Verify();
            Assert.IsTrue(expectedController.Disposed);
        }
        

        #endregion

        #endregion

        #region Helper class

        private class ViewModel
        {
            public int? ParentProperty { get; set; }

            [Cascading(Action = "TestController/TestAction", DependsOn = "ParentProperty")]
            public int CascadingProperty { get; set; }
        }

        private class SomeModel
        {
            public int Value { get; set; }
            public string Text { get; set; }
        }

        private class TestCascadingDropDownExtensionsHelper : CascadingDropDownExtensionsHelper
        {
            public override RouteData GetRouteData(string actionPath)
            {
                // We need to override this because we can't call it normally.
                var rd = new RouteData();
                rd.Values["action"] = "TestAction";
                rd.Values["controller"] = "TestController";
                return rd;
            }
        }

        private class TestController : ControllerBase, IDisposable
        {
            public bool Disposed { get; private set; }
            public IEnumerable<SomeModel> TestData { get; set; }

            public CascadingActionResult TestAction(int id)
            {
                // We don't care about the id argment for this test, but
                // it's here for consistency.
                return new CascadingActionResult() { Data = TestData, TextField = "Text", ValueField = "Value" };
            }

            protected override void ExecuteCore()
            {
                throw new NotImplementedException();
            }

            public void Dispose()
            {
                Disposed = true;
            }
        }

        #endregion

    }
}
